package com.sec.android.app.myfiles.operation.compress;

import android.content.Context;
import android.os.SystemClock;
import android.text.TextUtils;
import android.text.format.Formatter;

import com.sec.android.app.myfiles.R;
import com.sec.android.app.myfiles.feature.DlpMgr;
import com.sec.android.app.myfiles.info.AppFeatures;
import com.sec.android.app.myfiles.info.FileType;
import com.sec.android.app.myfiles.info.MediaFile;
import com.sec.android.app.myfiles.log.Log;
import com.sec.android.app.myfiles.module.abstraction.FileRecord;
import com.sec.android.app.myfiles.module.local.file.LocalFileRecord;
import com.sec.android.app.myfiles.operation.FileOperationException;
import com.sec.android.app.myfiles.util.SemFwWrapper;

import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.progress.ProgressMonitor;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ZipCompressorImp extends AbsCompressorImp {
    private static final long MAX_ZIP_SIZE = 4L * 1024 * 1024 * 1024;
    private static final int BUFFER_SIZE = 8192;

    private ArrayList<String> mDecompressFolderList;
    private ArrayList<String> mDecompressFileList;

    private int mTotalCount = 0;
    private int mCurrentCount = 0;

    public ZipCompressorImp(Context context, CompressOptions options) {
        super(context, options);
    }

    @Override
    public boolean _compress(ArrayList<FileRecord> srcList, FileRecord dst) throws FileOperationException {
        boolean ret = false;
        if (mOptions.mSrcRoot == null) {
            throwException(R.string.failed_to_compress);
            return false;
        }
        ArrayList<File> files = getAllSubFiles(srcList);
        mTotalCount = 0;
        for (File file : files) {
            if (file.isFile()) {
                mTotalCount++;
            }
        }
        File root = SemFwWrapper.file(mOptions.mSrcRoot.getFullPath());
        String target = dst.getFullPath();
        File targetFile = SemFwWrapper.file(target);

        if (targetFile.exists()) {
            dst = handleDuplicatedRecord(dst);
            if (dst != null) {
                target = dst.getFullPath();
            } else {
                throwCancelException();
            }
        }

        if (isCancelled()) {
            throwCancelException();
        } else {
            try {
                ret = addFilesToZip(files, root.isDirectory() ? root.getPath() : root.getParent(), target);
            } catch (FileOperationException e) {
                throw e;
            } finally {
                if (ret) {
                    DlpMgr.getInstance(mContext).setZipFileExpiry(targetFile);
                } else {
                    /* delete working file */
                    boolean result = SemFwWrapper.file(target).delete();
                    if (result)
                        Log.e(this, "Test _compress - target delete ");
                    else
                        Log.e(this, "delete - fail");
                }
            }
        }
        return ret;
    }

    @Override
    public boolean _decompress(FileRecord src, FileRecord dst) throws FileOperationException {
        if (!isValidZip(src)) {
            return false;
        }
        String targetFolderPath = getTargetPath(dst);
        try {
            ZipFile srcZipFile = createZipInstance(src.getFullPath(), true);

            List<FileHeader> headersList = getFileHeaders(srcZipFile);
            if (headersList != null) {
                HashMap<String, String> renamedFolderMap = new HashMap<>();
                HashSet<String> cancelledFolderSet = new HashSet<>();
                mTotalCount = headersList.size();
                boolean isAnyFileExtracted = false;
                mCurrentCount = 0;
                for (FileHeader fileHeader : headersList) {
                    if (isCancelled()) {
                        throwCancelException();
                        break;
                    }
                    mCurrentCount++;

                    convertRenamedFolderPath(fileHeader, renamedFolderMap);

                    String fileName = fileHeader.getFileName();
                    publishCountProgress(fileName, mCurrentCount, mTotalCount);

                    if (isCancelledItem(fileName, cancelledFolderSet)) {
                        continue;
                    }

                    if (!_decompressInternal(src, targetFolderPath, fileName, fileHeader, srcZipFile, renamedFolderMap, cancelledFolderSet)) {
                        continue;
                    }
                    isAnyFileExtracted = true;
                }
                if (!isAnyFileExtracted) {
                    throwCancelException();
                }
            }
        } catch (ZipException e) {
            handleException(e);
        } finally {
            DlpMgr.getInstance(mContext).restoreZipFileExpiry(src);
        }
        return true;
    }

    private void convertRenamedFolderPath(FileHeader fileHeader, HashMap<String, String> renamedFolderMap) {
        Set<String> keySet = renamedFolderMap.keySet();
        String zipItemPath = fileHeader.getFileName();
        for (String key : keySet) {
            if (zipItemPath.startsWith(key)) {
                zipItemPath = zipItemPath.replaceFirst(Pattern.quote(key), renamedFolderMap.get(key));
                fileHeader.setFileName(zipItemPath);
                break;
            }
        }
    }

    private String getTargetPath(FileRecord dst) throws FileOperationException {
        String targetFolderPath = dst.getFullPath();
        File targetFolder = SemFwWrapper.file(targetFolderPath);

        if (!mOptions.mDecompressToCurrentFolder) {
            if (targetFolder.exists()) {
                String name = targetFolder.getName();
                if (!".".equals(name) && !("..").equals(name)) {
                    targetFolderPath = makeNewFolder(targetFolderPath);
                } else {
                    try {
                        targetFolderPath = targetFolder.getCanonicalPath();
                        mResultExtras.putString(FINAL_DST_PATH, targetFolderPath);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return targetFolderPath;
    }

    private String makeNewFolder(String targetFolderPath) throws FileOperationException {
        String ret = handleDuplicated(targetFolderPath);
        if (ret != null) {
            mResultExtras.putString(FINAL_DST_PATH, ret);

            File newFolder = SemFwWrapper.file(ret);
            if (!newFolder.exists()) {
                if (!newFolder.mkdirs()) {
                    Log.d(this, "makeNewFolder targetFolder.mkdirs failed");
                }
            }
        } else {
            throwCancelException();
        }

        return ret;
    }


    private boolean _decompressInternal(FileRecord src, String targetFolderPath, String fileName, FileHeader fileHeader, ZipFile srcZipFile, HashMap<String, String> renamedFolderMap, HashSet<String> cancelledFolderSet)
            throws FileOperationException, ZipException {
        boolean bRet = false;
        StringBuilder dstFilePathWithName = new StringBuilder();
        StringBuilder dstFilePath = new StringBuilder();

        dstFilePathWithName.append(targetFolderPath).append(File.separator).append(fileName);
        dstFilePath.append(dstFilePathWithName.substring(0, dstFilePathWithName.lastIndexOf(File.separator) + 1));

        File dstFile = SemFwWrapper.file(dstFilePath.toString());

        if (isValidPath(dstFile, src)) {
            if (fileHeader.isDirectory()) {
                bRet = _decompressDirectory(targetFolderPath, dstFile, fileHeader, renamedFolderMap, cancelledFolderSet);
            } else {
                bRet = _decompressFile(dstFilePathWithName.toString(), srcZipFile, dstFile, dstFilePath.toString(), fileHeader);
            }
            if (bRet) {
                updateLastModifiedTime(fileHeader, dstFile);
            }
        } else {
            throwException(R.string.failed_to_extract);
        }

        return bRet;
    }

    private ZipFile createZipInstance(String path, boolean setRunInThread) throws ZipException {
        ZipFile ret = new ZipFile(path);
        if (!isUTF8Encoded(ret)) {
            ret = setCharset(mContext, ret);
        }

        if (setRunInThread) {
            ret.setRunInThread(true);
        }


        return ret;
    }

    @Override
    public boolean _decompress(FileRecord src, FileRecord dst, ArrayList<FileRecord> decompressList) throws FileOperationException {
        if (!isValidZip(src)) {
            return false;
        }
        String targetFolderPath = getTargetPath(dst);
        separateTargetList(decompressList);

        try {
            ZipFile srcZipFile = createZipInstance(src.getFullPath(), true);

            List<FileHeader> headersList = getFileHeaders(srcZipFile);
            if (headersList != null) {
                HashMap<String, String> renamedFolderMap = new HashMap<>();
                HashSet<String> cancelledFolderSet = new HashSet<>();
                mTotalCount = getTotalTargetCount(headersList);
                mCurrentCount = 0;
                boolean isAnyFileExtracted = false;
                for (FileHeader fileHeader : headersList) {
                    if (isCancelled()) {
                        throwCancelException();
                        break;
                    }

                    String fileName = fileHeader.getFileName();

                    // check this header is target
                    if (isTarget(fileName, fileHeader.isDirectory())) {
                        mCurrentCount++;
                        publishCountProgress(fileName, mCurrentCount, mTotalCount);

                        convertRenamedFolderPath(fileHeader, renamedFolderMap);

                        fileName = fileHeader.getFileName();
                        if (isCancelledItem(fileName, cancelledFolderSet)) {
                            continue;
                        }
                        if (!_decompressInternal(src, targetFolderPath, fileName, fileHeader, srcZipFile, renamedFolderMap, cancelledFolderSet)) {
                            continue;
                        }
                        isAnyFileExtracted = true;
                    }
                }
                if (!isAnyFileExtracted) {
                    throwCancelException();
                }
            }
        } catch (ZipException e) {
            handleException(e);
        } finally {
            DlpMgr.getInstance(mContext).restoreZipFileExpiry(src);
        }
        return true;
    }

    private void updateLastModifiedTime(FileHeader fileHeader, File dstFile) {
        int lastModified = fileHeader.getLastModFileTime();
        if (lastModified < 0) {
            lastModified = -lastModified;
        }

        if (!dstFile.setLastModified(dosToJavaTime(lastModified))) {
            Log.d(this, "updateLastModifiedTime : dstFile.setLastModified failed");
        }
    }


    private boolean _decompressFile(String dstFilePathWithName, ZipFile srcZipFile, File dstFile, String dstFilePath, FileHeader fileHeader) throws FileOperationException, ZipException {
        boolean bRet = true;
        File decompressFilePath = SemFwWrapper.file(dstFilePathWithName);
        if (decompressFilePath.exists()) {
            String targetFileName = handleDuplicated(decompressFilePath.getAbsolutePath());
            if (isCancelled()) {
                throwCancelException();
            }
            if (targetFileName == null) {
                bRet = false;
            } else {
                dstFile = SemFwWrapper.file(targetFileName);
            }
        } else {
            dstFile = decompressFilePath;
        }

        if (bRet) {
            srcZipFile.extractFile(fileHeader, dstFilePath, null, dstFile.getName());
            getResult(srcZipFile.getProgressMonitor(), dstFile, fileHeader.getFileName());
        }

        return bRet;
    }


    private boolean _decompressDirectory(String targetFolderPath, File dstFile, FileHeader fileHeader, HashMap<String, String> renamedFolderMap, HashSet<String> cancelledFolderSet) {
        if (!dstFile.exists()) {
            if (!dstFile.mkdirs()) {
                Log.e(this, "_decompress: dstFile.mkdirs() failed");
            }
        } else {
            String targetFolderName = handleDuplicated(dstFile.getAbsolutePath());
            if (targetFolderName != null) {
                dstFile = SemFwWrapper.file(targetFolderName);
                // renamed
                if (!dstFile.exists()) {
                    renamedFolderMap.put(fileHeader.getFileName(), targetFolderName.substring(targetFolderPath.length() + 1) + "/");
                }

                if (!dstFile.mkdirs()) {
                    Log.d(this, "_decompress: dstFile.mkdirs() failed");
                }
            } else {
                cancelledFolderSet.add(fileHeader.getFileName());
                return false;
            }
        }
        return true;
    }


    private void getResult(ProgressMonitor progressMonitor, File dstFile, String fileName) throws FileOperationException {
        if (progressMonitor.getResult() == ProgressMonitor.RESULT_SUCCESS) {
            publishCurFileProgress(100, 100);
            DlpMgr.getInstance(mContext).putDLPFilesMap(dstFile, fileName);
        }

        while (progressMonitor.getState() == ProgressMonitor.STATE_BUSY) {
            if (isCancelled()) {
                progressMonitor.cancelAllTasks();
                boolean result = dstFile.delete();
                if (result)
                    Log.e(this, "getResult - target delete ");
                else
                    Log.e(this, "getResult_delete - fail");
                throwCancelException();
            }
            publishCurFileProgress(progressMonitor.getPercentDone(), 100);
            SystemClock.sleep(10);
        }

        Throwable exception = progressMonitor.getException();
        if (exception != null) {
            handleException(exception);
        }
    }

    @Override
    public CompressedItem[] list(FileRecord src) throws FileOperationException {
        if (!isValidZip(src)) {
            return null;
        }

        CompressedItem[] ret = null;

        List<FileHeader> headersList;
        ArrayList<CompressedItem> zipItemFolderList;
        ArrayList<CompressedItem> zipItemList = null;
        HashMap<String, CompressedItem> childCountMap;

        try {
            if (isCancelled()) {
                throwCancelException();
            }
            ZipFile srcZipFile = createZipInstance(src.getFullPath(), false);

            headersList = getFileHeaders(srcZipFile);
            if (headersList != null) {
                zipItemFolderList = new ArrayList<>();
                zipItemList = new ArrayList<>();
                childCountMap = new HashMap<>();

                addFolderAndItemsToList(headersList, src, zipItemFolderList, childCountMap, zipItemList);
                incrementChildCounts(childCountMap, zipItemFolderList, zipItemList);
                zipItemList.addAll(zipItemFolderList);
            }
        } catch (ZipException e) {
            handleException(e);
        }
        if (zipItemList != null) {
            int size = zipItemList.size();
            ret = new CompressedItem[size];
            if (size > 0) {
                ret = zipItemList.toArray(ret);
            } else {
                Log.d(this, "itemList is empty");
            }
        }
        return ret;
    }

    private void addFolderAndItemsToList(List<FileHeader> headersList, FileRecord src, ArrayList<CompressedItem> zipItemFolderList,
                                         HashMap<String, CompressedItem> childCountMap, ArrayList<CompressedItem> zipItemList) throws FileOperationException {

        CompressedItem compressedItem;

        for (FileHeader fileHeader : headersList) {
            if (isCancelled()) {
                throwCancelException();
                break;
            }
            if (isValidZipEntryPath(fileHeader, src)) {
                compressedItem = getCompressedItem(fileHeader);
                // some folder information are not exist in some zip file.
                // so find all parent folder information from exist files.
                getParents(compressedItem, zipItemFolderList, childCountMap);
                if (compressedItem.mFileType == FileType.FOLDER) {
                    zipItemFolderList.add(compressedItem);
                    childCountMap.put(compressedItem.mFullPath, compressedItem);
                } else {
                    zipItemList.add(compressedItem);
                }
            }
        }
    }

    private void incrementChildCounts(HashMap<String, CompressedItem> childCountMap, ArrayList<CompressedItem> zipItemFolderList, ArrayList<CompressedItem> zipItemList) {
        CompressedItem savedFolder;
        for (CompressedItem item : zipItemFolderList) {
            savedFolder = childCountMap.get(item.mPath);
            if (savedFolder != null) {
                savedFolder.mChildCount++;
                savedFolder.mHiddenChildCount++;
            }
        }

        for (CompressedItem item : zipItemList) {
            savedFolder = childCountMap.get(item.mPath);
            if (savedFolder != null) {
                savedFolder.mChildCount++;
                savedFolder.mHiddenChildCount++;
            }
        }
    }

    private boolean isValidPath(File targetZipEntry, FileRecord archive) {
        boolean ret = false;
        try {
            String targetFilePath = targetZipEntry.getCanonicalPath();
            String archiveParentPath = archive.getPath();
            if (targetFilePath != null && archiveParentPath != null) {
                ret = targetFilePath.startsWith(archiveParentPath);
            }
        } catch (IOException e) {
            Log.e(this, "IOException:" + e.toString());
        }
        return ret;
    }

    private boolean isValidZipEntryPath(FileHeader fileHeader, FileRecord archive) {
        boolean ret = false;
        String zipEntryPath = fileHeader.getFileName();
        String parent = archive.getPath();
        if (zipEntryPath != null && parent != null) {
            if (zipEntryPath.charAt(0) != '/') {
                try {
                    String realPath = SemFwWrapper.file(parent + "/" + zipEntryPath).getCanonicalPath();
                    if (realPath != null && realPath.startsWith(parent)) {
                        ret = true;
                    }
                } catch (IOException e) {
                    Log.e(this, "IOException:" + e.toString());
                }
            }
        }
        return ret;
    }

    private void getParents(CompressedItem compressedItem, ArrayList<CompressedItem> zipItemParentList,
                            HashMap<String, CompressedItem> childCountMap) {
        CompressedItem folderItem;
        String parentPath = compressedItem.mPath;
        if (!TextUtils.isEmpty(parentPath)) {
            int index = parentPath.lastIndexOf('/');
            if (index == -1) {
                if (!isExistFolder(zipItemParentList, parentPath)) {
                    folderItem = createCompressedFolderItem(parentPath);
                    zipItemParentList.add(folderItem);
                    childCountMap.put(parentPath, folderItem);
                }
            } else {
                String[] paths = parentPath.split("/");
                StringBuilder sb = new StringBuilder();
                for (String path : paths) {
                    sb.append(path);
                    if (!isExistFolder(zipItemParentList, sb.toString())) {
                        folderItem = createCompressedFolderItem(sb.toString());
                        zipItemParentList.add(folderItem);
                        childCountMap.put(folderItem.mFullPath, folderItem);
                    }
                    sb.append('/');
                }
            }
        }
    }

    private boolean isExistFolder(ArrayList<CompressedItem> zipItemParentList, String targetPath) {
        boolean found = false;
        for (CompressedItem item : zipItemParentList) {
            if (item.mFullPath.equals(targetPath)) {
                found = true;
                break;
            }
        }
        return found;
    }

    private CompressedItem createCompressedFolderItem(String path) {
        CompressedItem compressedItem = new CompressedItem();
        compressedItem.mFileType = FileType.FOLDER;
        compressedItem.mFullPath = path;
        int index = path.lastIndexOf('/');
        if (index == -1) {
            compressedItem.mName = path;
        } else {
            compressedItem.mPath = path.substring(0, index);
            compressedItem.mName = path.substring(index + 1);
        }
        compressedItem.mDate = System.currentTimeMillis();
        return compressedItem;
    }

    private CompressedItem getCompressedItem(FileHeader fileHeader) {
        String fullPath;
        boolean isDirectory;
        CompressedItem compressedItem = new CompressedItem();
        fullPath = fileHeader.getFileName();
        isDirectory = fileHeader.isDirectory();
        if (isDirectory) {
            fullPath = fullPath.substring(0, fullPath.length() - 1);
            compressedItem.mFileType = FileType.FOLDER;
        } else {
            compressedItem.mFileType = MediaFile.getFileType(fullPath);
            compressedItem.mExt = MediaFile.getExtensionAsUpperCase(fullPath);
        }
        compressedItem.mFullPath = fullPath;
        int index = fullPath.lastIndexOf('/');
        if (index == -1) {
            compressedItem.mName = fullPath;
        } else {
            compressedItem.mPath = fullPath.substring(0, index);
            compressedItem.mName = fullPath.substring(index + 1);
        }
        compressedItem.mSize = fileHeader.getUncompressedSize();
        compressedItem.mDate = dosToJavaTime(fileHeader.getLastModFileTime());
        return compressedItem;
    }

    @Override
    public void _postOperation(boolean result, FileRecord dst) {
    }

    /**
     * This method perforce is displaying current / total item count. And prevent infinite loop when do a copy with recursive logic.<br>
     * (Because of this Legacy MyFiles did block to copy some file into it's child directory.)
     */
    ArrayList<File> getAllSubFiles(ArrayList<FileRecord> records) {
        ArrayList<File> allFiles = new ArrayList<>();
        try {
            for (FileRecord record : records) {
                addSubFiles(allFiles, SemFwWrapper.file(record.getFullPath()));
            }
        } catch (StackOverflowError e) {
            // TODO show a toast? Like a directory depth is too deep?
            Log.e(this, "StackOverflowError:" + e.toString());
        }
        return allFiles;
    }

    /**
     * Caution!
     * <p>
     * This is recursive logic, so if the directory depth is too deep for current VM, it will make StackOverflowError
     *
     * @param files result list of source file that has all sub files of src
     * @param src   source file to get the all sub files
     * @throws StackOverflowError
     */
    void addSubFiles(ArrayList<File> files, File src) throws StackOverflowError {
        files.add(src);
        if (src.isDirectory()) {
            File[] sub = src.listFiles();
            if (sub == null || sub.length == 0) {
                // TODO need a error handling?
                Log.d(this, "nothing in this folder " + src);
            } else {
                for (File subSrc : sub) {
                    addSubFiles(files, subSrc);
                }
            }
        }
    }

    private boolean addFilesToZip(ArrayList<File> files, String zipRootPath, String target) throws FileOperationException {
        boolean ret = true;
        File zipFile = SemFwWrapper.file(target);
        String zipEntryPath;
        ZipEntry zipEntry;

        if (!TextUtils.isEmpty(zipRootPath)) {
            if (!File.separator.equals(String.valueOf(zipRootPath.charAt(zipRootPath.length() - 1)))) {
                zipRootPath = zipRootPath + File.separator;
            }
        }

        try (FileOutputStream fileOutputStream = SemFwWrapper.outputStream(zipFile);
             ZipOutputStream zos = new ZipOutputStream(fileOutputStream)) {
            mCurrentCount = 0;
            for (File file : files) {
                if (isCancelled()) {
                    if (zipFile.exists() && !zipFile.delete()) {
                        Log.d(this, "addFilesToZip : zipFile.delete() failed");
                    }
                    throwCancelException();
                    return false;
                }
                if (file.isFile()) {
                    mCurrentCount++;
                }
                publishCountProgress(file.getName(), mCurrentCount, mTotalCount);
                zipEntryPath = file.getAbsolutePath();
                zipEntryPath = zipEntryPath.substring(zipEntryPath.indexOf(zipRootPath) + zipRootPath.length());

                if (file.isDirectory()) {
                    zipEntry = new ZipEntry(zipEntryPath + File.separator);
                    zipEntry.setTime(file.lastModified());
                    zos.putNextEntry(zipEntry);
                    continue;
                } else {
                    zipEntry = new ZipEntry(zipEntryPath);
                    zipEntry.setTime(file.lastModified());

                    DlpMgr.getInstance(mContext).setExtraToZipEntry(zipEntry, file.getAbsolutePath());
                    zos.putNextEntry(zipEntry);
                }

                long bytesRead = 0;
                byte[] bytesIn = new byte[BUFFER_SIZE];
                int read;
                long totalBytes = file.length();

                if (totalBytes > 0L) {
                    try (FileInputStream fileInputStream = SemFwWrapper.inputStream(file);
                         BufferedInputStream bis = new BufferedInputStream(fileInputStream)) {
                        while ((read = bis.read(bytesIn)) != -1) {
                            if (isCancelled()) {
                                if (zipFile.exists() && !zipFile.delete()) {
                                    Log.d(this, "addFilesToZip: delete zipFile failed");
                                }
                                throwCancelException();
                                return false;
                            }
                            zos.write(bytesIn, 0, read);
                            bytesRead += read;
                            publishCurFileProgress((int) (bytesRead * 100 / totalBytes), 100);
                        }
                        if (zipFile.length() > MAX_ZIP_SIZE) {
                            if (zipFile.exists() && !zipFile.delete()) {
                                Log.d(this, "addFilesToZip: zipFile.delete() failed");
                            }
                            String maxSize = Formatter.formatShortFileSize(mContext, MAX_ZIP_SIZE);
                            String msg = mContext.getString(R.string.cant_zip_more_than_ps_of_files_at_one_time, maxSize);
                            throwException(msg, new Exception("zip file length larger than " + maxSize));
                            return false;
                        }
                    }
                }
            }
        } catch (FileNotFoundException e) {
            throwException(R.string.file_does_not_exist, e);
            ret = false;
        } catch (IOException e) {
            handleException(e);
            ret = false;
        }
        return ret;
    }

    private boolean isValidZip(FileRecord src) throws FileOperationException {
        String fullPath = src.getFullPath();
        File srcFile = SemFwWrapper.file(src.getFullPath());
        ZipFile zipFile = null;
        boolean ret = true;

        // check the exist of file.
        if (!srcFile.exists()) {
            throwException(R.string.file_does_not_exist);
            return false;
        }

        if (AppFeatures.blockZipErrorModel()) {
            try {
                zipFile = new ZipFile(fullPath);
                ret = zipFile.isValidZipFile();
                if (ret == false) {
                    throwException(R.string.failed_to_extract);
                }
            } catch (net.lingala.zip4j.exception.ZipException e) {
                ret = false;
                throwException(R.string.failed_to_extract, e);
                Log.e(this, "net.lingala.zip4j.exception.ZipException:" + e.toString());
            } catch (Exception e) {
                ret = false;
                throwException(R.string.failed_to_extract, e);
                Log.e(this, "Exception:" + e.toString());
            }
        } else {
            // check zip exception via java.util.zip.ZipFile
            try (java.util.zip.ZipFile zipFileForTest = new java.util.zip.ZipFile(srcFile)) {
                zipFile = new ZipFile(fullPath);
                ret = zipFile.isValidZipFile();
            } catch (java.util.zip.ZipException e) {
                ret = false;
                throwException(R.string.failed_to_extract, e);
                Log.e(this, "java.util.zip.ZipException:" + e.toString());
            } catch (IOException e) {
                ret = false;
                handleException(e);
            } catch (net.lingala.zip4j.exception.ZipException e) {
                ret = false;
                throwException(R.string.failed_to_extract, e);
                Log.e(this, "net.lingala.zip4j.exception.ZipException:" + e.toString());
            }
        }

        return ret;
    }

    private void handleException(Throwable e) throws FileOperationException {
        int failMessageResId = R.string.failed_to_compress;
        if (mOptions.mCompressMode == CompressOptions.CompressMode.DECOMPRESS) {
            failMessageResId = R.string.failed_to_extract;
        }
        Log.e(this, "Exception:" + e.toString());
        if (e.getCause() != null && e.getCause().toString() != null) {
            if (e.getCause().toString().contains("ENOSPC")) {
                throwException(R.string.not_enough_memory_popup, e);
            } else {
                throwException(failMessageResId, e);
            }
        } else {
            throwException(failMessageResId, e);
        }
    }

    private boolean isUTF8Encoded(ZipFile zipFile) {
        boolean foundUTF8 = false;
        if (zipFile != null) {
            try {
                List<FileHeader> headersList = getFileHeaders(zipFile);
                if (headersList != null) {
                    for (FileHeader fileHeader : headersList) {
                        if (fileHeader != null) {
                            if (fileHeader.isFileNameUTF8Encoded()) {
                                foundUTF8 = true;
                                break;
                            }
                        }
                    }
                }
            } catch (ZipException e) {
                Log.e(this, "ZipException:" + e.toString());
            }
        }
        return foundUTF8;
    }

    @SuppressWarnings("unchecked")
    private List<FileHeader> getFileHeaders(ZipFile zipFile) throws ZipException {
        List<FileHeader> ret = null;
        if (zipFile != null) {
            ret = zipFile.getFileHeaders();
        }
        return ret;
    }

    private ZipFile setCharset(Context context, ZipFile zipFile) {
        ZipFile ret = zipFile;
        if (zipFile != null) {
            String curLocale = context.getResources().getConfiguration().locale.getLanguage();
            try {
                String charset = null;
                if (curLocale.equals(Locale.JAPANESE.getLanguage()) || curLocale.equals(Locale.JAPAN.getLanguage())) {
                    charset = "Shift_JIS";
                } else if (curLocale.equals(Locale.KOREA.getLanguage()) || curLocale.equals(Locale.KOREAN.getLanguage())) {
                    charset = "EUC-KR";
                } else if (curLocale.equals(Locale.CHINA.getLanguage()) || curLocale.equals(Locale.CHINESE.getLanguage())) {
                    charset = "GBK";
                }
                if (charset != null) {
                    ret = new ZipFile(zipFile.getFile().getAbsolutePath());
                    ret.setFileNameCharset(charset);
                }
            } catch (ZipException e) {
                Log.e(this, "ZipException:" + e.toString());
            }
        }
        return ret;
    }

    private String handleDuplicated(String path) {
        String ret = null;
        FileRecord record = handleDuplicatedRecord(new LocalFileRecord(path));
        if (record != null) {
            ret = record.getFullPath();
        }
        return ret;
    }

    private void separateTargetList(ArrayList<FileRecord> decompressList) {
        mDecompressFolderList = new ArrayList<>();
        mDecompressFileList = new ArrayList<>();

        for (FileRecord record : decompressList) {
            if (record.isDirectory()) {
                mDecompressFolderList.add(record.getFullPath());
            } else {
                mDecompressFileList.add(record.getFullPath());
            }
        }
    }

    private int getTotalTargetCount(List<FileHeader> headerList) {
        ArrayList<String> decompressFileListBackup = new ArrayList<>(mDecompressFileList);
        ArrayList<String> decompressFolderListBackup = new ArrayList<>(mDecompressFolderList);
        int result = 0;
        for (FileHeader header : headerList) {
            if (isTarget(header.getFileName(), header.isDirectory())) {
                result++;
            }
        }
        mDecompressFileList = new ArrayList<>(decompressFileListBackup);
        mDecompressFolderList = new ArrayList<>(decompressFolderListBackup);
        return result;
    }

    private boolean isTarget(String itemName, boolean isDirectory) {
        boolean ret = false;
        Iterator<String> it;
        if (!isDirectory) {
            it = mDecompressFileList.iterator();
            while (it.hasNext()) {
                if (it.next().equals(itemName)) {
                    it.remove();
                    ret = true;
                    break;
                }
            }
        }
        String targetFolder;
        it = mDecompressFolderList.iterator();
        itemName = itemName.substring(0, itemName.length() - 1);
        boolean isTarget = false;
        while (it.hasNext() && !isTarget) {
            targetFolder = it.next();
            if (targetFolder != null) {
                isTarget = itemName.indexOf('/') > 0 ? (itemName.startsWith(targetFolder)) : targetFolder.equals(itemName);
            }
        }
        return isTarget || ret;
    }

    private boolean isCancelledItem(String fileName, HashSet<String> cancelledFolderSet) {
        boolean ret = false;
        for (String folder : cancelledFolderSet) {
            if (fileName.equals(folder) || fileName.startsWith(folder)) {
                ret = true;
                break;
            }
        }
        return ret;
    }

    public long dosToJavaTime(int dosTime) {
        int sec = 2 * (dosTime & 0x1f);
        int min = (dosTime >> 5) & 0x3f;
        int hrs = (dosTime >> 11) & 0x1f;
        int day = (dosTime >> 16) & 0x1f;
        int mon = ((dosTime >> 21) & 0xf) - 1;
        int year = ((dosTime >> 25) & 0x7f) + 1980;

        Calendar cal = Calendar.getInstance();
        cal.set(year, mon, day, hrs, min, sec);

        return cal.getTime().getTime();
    }

}
